useObservable
Scripting provides a reactive state system formed by Observable<T> and the useObservable<T> hook.
This system drives UI updates, interacts with the animation engine, and aligns closely with SwiftUI’s binding model—enabling future APIs such as List(selection:), NavigationStack(path:), TextField(text:), and more.
1. Observable<T>
Observable<T> is a reactive container that holds a mutable value.
Whenever the value changes, any UI components that read this value are automatically re-rendered.
1.1 Class Definition
1class Observable<T> {
2 constructor(initialValue: T);
3 value: T;
4 setValue(value: T): void;
5 subscribe(callback: (value: T, oldValue: T) => void): void;
6 unsubscribe(callback: (value: T, oldValue: T) => void): void;
7 dispose(): void;
8}
1.2 Property & Method Details
value
The current value stored inside the observable.
setValue(newValue)
Updates the value and triggers UI re-rendering.
1observable.setValue(newValue);
T may be any type: primitives, arrays, objects, or class instances.
subscribe / unsubscribe
Allows external listeners to respond to value changes.
Most components do not need to use these manually.
dispose
Releases internal subscriptions.
Typically only needed when manually managing observables outside the component system.
2. useObservable<T>
useObservable<T> creates component-local reactive state and provides an Observable<T> instance whose value persists across re-renders.
2.1 Function Signature
1declare function useObservable<T>(): Observable<T | undefined>;
2declare function useObservable<T>(value: T): Observable<T>;
3declare function useObservable<T>(initializer: () => T): Observable<T>;
2.2 Initialization Modes
1. Without initial value
Value defaults to undefined.
1const data = useObservable<string>();
2. With initial value
1const count = useObservable(0);
3. Lazy initialization
The initializer is executed only on the first render.
1const user = useObservable(() => createDefaultUser());
3. Using Observable in UI Components
Reading .value inside JSX automatically establishes dependency tracking.
1<Text>{name.value}</Text>
Updating the state triggers re-render:
1<Button title="Change" action={() => name.setValue("Updated")} />
This behavior is similar to React’s useState, but aligned with SwiftUI’s reactive identity-based rendering.
4. Integration with Animation
Observable values participate directly in Scripting’s animation system.
There are two main animation mechanisms:
4.1 Explicit animations: withAnimation
1withAnimation(() => {
2 size.setValue(size.value + 20);
3});
Any view that depends on size.value will animate its change.
4.2 Implicit animations: the animation modifier
Views can animate whenever a specific dependency changes.
Correct syntax:
1animation={{
2 animation: Animation.spring({ duration: 0.3 }),
3 value: size.value
4}}
This mirrors SwiftUI’s .animation(animation, value: value) API.
Example:
1<Rectangle
2 frame={{
3 width: size.value,
4 height: size.value,
5 }}
6 animation={{
7 animation: Animation.easeIn(0.25),
8 value: size.value,
9 }}
10/>
5. Forward Compatibility with SwiftUI-Style Binding APIs
Observable is the foundation for future SwiftUI-style binding APIs.
Upcoming components will accept Observable<T> directly, matching SwiftUI’s $binding behavior.
5.1 List(selection:)
1const selection = useObservable<string | undefined>(undefined)
2
3<List selection={selection}>
4 ...
5</List>
5.2 NavigationStack(path:)
1const path = useObservable<string[]>([])
2
3<NavigationStack path={path}>
4 ...
5</NavigationStack>
This allows fully type-safe and reactive navigation, mirroring SwiftUI’s native patterns.
6. ForEach: Recommended Data Binding Pattern
Scripting provides a SwiftUI-aligned ForEach API:
1<ForEach data={items} builder={(item, index) => <Text>{item.name}</Text>} />
Where each item must satisfy:
1T extends { id: string }
Why this is the recommended pattern:
- Enables insertion/removal animations
- Avoids index-based rendering issues
- Improves performance for large lists
Example:
1const items = useObservable([
2 { id: "1", name: "Apple" },
3 { id: "2", name: "Banana" }
4])
5
6<ForEach
7 data={items}
8 editActions="all"
9 builder={(item) => <Text>{item.name}</Text>}
10/>
7. Complete Example
1export function Demo() {
2 const visible = useObservable(true);
3 const size = useObservable(100);
4
5 return (
6 <VStack spacing={20}>
7 {visible.value && (
8 <Rectangle
9 frame={{
10 width: size.value,
11 height: size.value,
12 }}
13 background="blue"
14 animation={{
15 animation: Animation.spring({ duration: 0.4, bounce: 0.3 }),
16 value: size.value,
17 }}
18 transition={Transition.opacity()}
19 />
20 )}
21
22 <Button
23 title="Toggle Visible"
24 action={() => {
25 withAnimation(() => {
26 visible.setValue(!visible.value);
27 });
28 }}
29 />
30
31 <Button
32 title="Resize"
33 action={() => {
34 withAnimation(Animation.easeOut(0.25), () => {
35 size.setValue(size.value === 100 ? 160 : 100);
36 });
37 }}
38 />
39 </VStack>
40 );
41}
8. Summary
Observable<T> is the core reactive state container in Scripting.
useObservable creates component-local observable state.
- Any change to
.value automatically re-renders dependent UI.
- Observable integrates directly with animations (explicit and implicit).
- It is the foundation for SwiftUI-style binding APIs such as
List(selection:) and NavigationStack(path:).
- ForEach works best with
data: Observable<Array<T>> for identity-based diffing and smooth animations.